# frozen_string_literal: true

require 'test/unit'

class TestBox < Test::Unit::TestCase
  EXPERIMENTAL_WARNINGS = [
    "warning: Ruby::Box is experimental, and the behavior may change in the future!",
    "See doc/language/box.md for known issues, etc."
  ].join("\n")
  ENV_ENABLE_BOX = {'RUBY_BOX' => '1', 'TEST_DIR' => __dir__}

  def setup
    @n = nil
    @dir = __dir__
  end

  def teardown
    @n = nil
  end

  def test_box_availability_in_default
    assert_separately([], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      assert_nil ENV['RUBY_BOX']
      assert !Ruby::Box.enabled?
    end;
  end

  def test_box_availability_when_enabled
    assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      assert '1', ENV['RUBY_BOX']
      assert Ruby::Box.enabled?
    end;
  end

  def test_current_box_in_main
    assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      assert_equal Ruby::Box.main, Ruby::Box.current
      assert Ruby::Box.main.main?
    end;
  end

  def test_require_rb_separately
    pend unless Ruby::Box.enabled?

    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }

    @n.require(File.join(__dir__, 'namespace', 'a.1_1_0'))

    assert_not_nil @n::NS_A
    assert_not_nil @n::NS_B
    assert_equal "1.1.0", @n::NS_A::VERSION
    assert_equal "yay 1.1.0", @n::NS_A.new.yay
    assert_equal "1.1.0", @n::NS_B::VERSION
    assert_equal "yay_b1", @n::NS_B.yay

    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }
  end

  def test_require_relative_rb_separately
    pend unless Ruby::Box.enabled?

    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }

    @n.require_relative('namespace/a.1_1_0')

    assert_not_nil @n::NS_A
    assert_not_nil @n::NS_B
    assert_equal "1.1.0", @n::NS_A::VERSION
    assert_equal "yay 1.1.0", @n::NS_A.new.yay
    assert_equal "1.1.0", @n::NS_B::VERSION
    assert_equal "yay_b1", @n::NS_B.yay

    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }
  end

  def test_load_separately
    pend unless Ruby::Box.enabled?

    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }

    @n.load(File.join(__dir__, 'namespace', 'a.1_1_0.rb'))

    assert_not_nil @n::NS_A
    assert_not_nil @n::NS_B
    assert_equal "1.1.0", @n::NS_A::VERSION
    assert_equal "yay 1.1.0", @n::NS_A.new.yay
    assert_equal "1.1.0", @n::NS_B::VERSION
    assert_equal "yay_b1", @n::NS_B.yay

    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }
  end

  def test_box_in_box
    pend unless Ruby::Box.enabled?

    assert_raise(NameError) { NS1 }
    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }

    @n.require_relative('namespace/ns')

    assert_not_nil @n::NS1
    assert_not_nil @n::NS1::NS_A
    assert_not_nil @n::NS1::NS_B
    assert_equal "1.1.0", @n::NS1::NS_A::VERSION
    assert_equal "yay 1.1.0", @n::NS1::NS_A.new.yay
    assert_equal "1.1.0", @n::NS1::NS_B::VERSION
    assert_equal "yay_b1", @n::NS1::NS_B.yay

    assert_raise(NameError) { NS1 }
    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }
  end

  def test_require_rb_2versions
    pend unless Ruby::Box.enabled?

    assert_raise(NameError) { NS_A }

    @n.require(File.join(__dir__, 'namespace', 'a.1_2_0'))
    assert_equal "1.2.0", @n::NS_A::VERSION
    assert_equal "yay 1.2.0", @n::NS_A.new.yay

    n2 = Namespace.new
    n2.require(File.join(__dir__, 'namespace', 'a.1_1_0'))
    assert_equal "1.1.0", n2::NS_A::VERSION
    assert_equal "yay 1.1.0", n2::NS_A.new.yay

    # recheck @n is not affected by the following require
    assert_equal "1.2.0", @n::NS_A::VERSION
    assert_equal "yay 1.2.0", @n::NS_A.new.yay

    assert_raise(NameError) { NS_A }
  end

  def test_raising_errors_in_require
    pend unless Ruby::Box.enabled?

    assert_raise(RuntimeError, "Yay!") { @n.require(File.join(__dir__, 'namespace', 'raise')) }
    assert Namespace.current.inspect.include?("main")
  end

  def test_autoload_in_box
    pend unless Ruby::Box.enabled?

    assert_raise(NameError) { NS_A }

    @n.require_relative('namespace/autoloading')
    # autoloaded A is visible from global
    assert_equal '1.1.0', @n::NS_A::VERSION

    assert_raise(NameError) { NS_A }

    # autoload trigger NS_B::BAR is valid even from global
    assert_equal 'bar_b1', @n::NS_B::BAR

    assert_raise(NameError) { NS_A }
    assert_raise(NameError) { NS_B }
  end

  def test_continuous_top_level_method_in_a_box
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/define_toplevel')
    @n.require_relative('namespace/call_toplevel')

    assert_raise(NameError) { foo }
  end

  def test_top_level_methods_in_box
    pend # TODO: fix loading/current box detection
    pend unless Ruby::Box.enabled?
    @n.require_relative('box/top_level')
    assert_equal "yay!", @n::Foo.foo
    assert_raise(NameError) { yaaay }
    assert_equal "foo", @n::Bar.bar
    assert_raise_with_message(RuntimeError, "boooo") { @n::Baz.baz }
  end

  def test_proc_defined_in_box_refers_module_in_box
    pend unless Ruby::Box.enabled?

    # require_relative dosn't work well in assert_separately even with __FILE__ and __LINE__
    assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      ns1 = Namespace.new
      ns1.require(File.join("#{here}", 'namespace/proc_callee'))
      proc_v = ns1::Foo.callee
      assert_raise(NameError) { Target }
      assert ns1::Target
      assert_equal "fooooo", proc_v.call # refers Target in the namespace ns1
      ns1.require(File.join("#{here}", 'namespace/proc_caller'))
      assert_equal "fooooo", ns1::Bar.caller(proc_v)

      ns2 = Namespace.new
      ns2.require(File.join("#{here}", 'namespace/proc_caller'))
      assert_raise(NameError) { ns2::Target }
      assert_equal "fooooo", ns2::Bar.caller(proc_v) # refers Target in the namespace ns1
    end;
  end

  def test_proc_defined_globally_refers_global_module
    pend unless Ruby::Box.enabled?

    # require_relative dosn't work well in assert_separately even with __FILE__ and __LINE__
    assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      require(File.join("#{here}", 'namespace/proc_callee'))
      def Target.foo
        "yay"
      end
      proc_v = Foo.callee
      assert Target
      assert_equal "yay", proc_v.call # refers global Foo
      ns1 = Namespace.new
      ns1.require(File.join("#{here}", 'namespace/proc_caller'))
      assert_equal "yay", ns1::Bar.caller(proc_v)

      ns2 = Namespace.new
      ns2.require(File.join("#{here}", 'namespace/proc_callee'))
      ns2.require(File.join("#{here}", 'namespace/proc_caller'))
      assert_equal "fooooo", ns2::Foo.callee.call
      assert_equal "yay", ns2::Bar.caller(proc_v) # should refer the global Target, not Foo in ns2
    end;
  end

  def test_instance_variable
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/instance_variables')

    assert_equal [], String.instance_variables
    assert_equal [:@str_ivar1, :@str_ivar2], @n::StringDelegatorObj.instance_variables
    assert_equal 111, @n::StringDelegatorObj.str_ivar1
    assert_equal 222, @n::StringDelegatorObj.str_ivar2
    assert_equal 222, @n::StringDelegatorObj.instance_variable_get(:@str_ivar2)

    @n::StringDelegatorObj.instance_variable_set(:@str_ivar3, 333)
    assert_equal 333, @n::StringDelegatorObj.instance_variable_get(:@str_ivar3)
    @n::StringDelegatorObj.remove_instance_variable(:@str_ivar1)
    assert_nil @n::StringDelegatorObj.str_ivar1
    assert_equal [:@str_ivar2, :@str_ivar3], @n::StringDelegatorObj.instance_variables

    assert_equal [], String.instance_variables
  end

  def test_methods_added_in_box_are_invisible_globally
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/string_ext')

    assert_equal "yay", @n::Bar.yay

    assert_raise(NoMethodError){ String.new.yay }
  end

  def test_continuous_method_definitions_in_a_box
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/string_ext')
    assert_equal "yay", @n::Bar.yay

    @n.require_relative('namespace/string_ext_caller')
    assert_equal "yay", @n::Foo.yay

    @n.require_relative('namespace/string_ext_calling')
  end

  def test_methods_added_in_box_later_than_caller_code
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/string_ext_caller')
    @n.require_relative('namespace/string_ext')

    assert_equal "yay", @n::Bar.yay
    assert_equal "yay", @n::Foo.yay
  end

  def test_method_added_in_box_are_available_on_eval
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/string_ext')
    @n.require_relative('namespace/string_ext_eval_caller')

    assert_equal "yay", @n::Baz.yay
  end

  def test_method_added_in_box_are_available_on_eval_with_binding
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/string_ext')
    @n.require_relative('namespace/string_ext_eval_caller')

    assert_equal "yay, yay!", @n::Baz.yay_with_binding
  end

  def test_methods_and_constants_added_by_include
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/open_class_with_include')

    assert_equal "I'm saying foo 1", @n::OpenClassWithInclude.say
    assert_equal "I'm saying foo 1", @n::OpenClassWithInclude.say_foo
    assert_equal "I'm saying foo 1", @n::OpenClassWithInclude.say_with_obj("wow")

    assert_raise(NameError) { String::FOO }

    assert_equal "foo 1", @n::OpenClassWithInclude.refer_foo
  end
end

module ProcLookupTestA
  module B
    VALUE = 111
  end
end

class TestBox < Test::Unit::TestCase
  def make_proc_from_block(&b)
    b
  end

  def test_proc_from_main_works_with_global_definitions
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/procs')

    proc_and_labels = [
      [Proc.new { String.new.yay }, "Proc.new"],
      [proc { String.new.yay }, "proc{}"],
      [lambda { String.new.yay }, "lambda{}"],
      [->(){ String.new.yay }, "->(){}"],
      [make_proc_from_block { String.new.yay }, "make_proc_from_block"],
      [@n::ProcInNS.make_proc_from_block { String.new.yay }, "make_proc_from_block in @n"],
    ]

    proc_and_labels.each do |str_pr|
      pr, pr_label = str_pr
      assert_raise(NoMethodError, "NoMethodError expected: #{pr_label}, called in main") { pr.call }
      assert_raise(NoMethodError, "NoMethodError expected: #{pr_label}, called in @n") { @n::ProcInNS.call_proc(pr) }
    end

    const_and_labels = [
      [Proc.new { ProcLookupTestA::B::VALUE }, "Proc.new"],
      [proc { ProcLookupTestA::B::VALUE }, "proc{}"],
      [lambda { ProcLookupTestA::B::VALUE }, "lambda{}"],
      [->(){ ProcLookupTestA::B::VALUE }, "->(){}"],
      [make_proc_from_block { ProcLookupTestA::B::VALUE }, "make_proc_from_block"],
      [@n::ProcInNS.make_proc_from_block { ProcLookupTestA::B::VALUE }, "make_proc_from_block in @n"],
    ]

    const_and_labels.each do |const_pr|
      pr, pr_label = const_pr
      assert_equal 111, pr.call, "111 expected, #{pr_label} called in main"
      assert_equal 111, @n::ProcInNS.call_proc(pr), "111 expected, #{pr_label} called in @n"
    end
  end

  def test_proc_from_box_works_with_definitions_in_box
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/procs')

    proc_types = [:proc_new, :proc_f, :lambda_f, :lambda_l, :block]

    proc_types.each do |proc_type|
      assert_equal 222, @n::ProcInNS.make_const_proc(proc_type).call, "ProcLookupTestA::B::VALUE should be 222 in @n"
      assert_equal "foo", @n::ProcInNS.make_str_const_proc(proc_type).call, "String::FOO should be \"foo\" in @n"
      assert_equal "yay", @n::ProcInNS.make_str_proc(proc_type).call, "String#yay should be callable in @n"
      #
      # TODO: method calls not-in-methods nor procs can't handle the current namespace correctly.
      #
      # assert_equal "yay,foo,222",
      #              @n::ProcInNS.const_get(('CONST_' + proc_type.to_s.upcase).to_sym).call,
      #              "Proc assigned to constants should refer constants correctly in @n"
    end
  end

  def test_class_module_singleton_methods
    pend unless Ruby::Box.enabled?

    @n.require_relative('namespace/singleton_methods')

    assert_equal "Good evening!", @n::SingletonMethods.string_greeing # def self.greeting
    assert_equal 42, @n::SingletonMethods.integer_answer # class << self; def answer
    assert_equal([], @n::SingletonMethods.array_blank) # def self.blank w/ instance methods
    assert_equal({status: 200, body: 'OK'}, @n::SingletonMethods.hash_http_200) # class << self; def ... w/ instance methods

    assert_equal([4, 4], @n::SingletonMethods.array_instance_methods_return_size([1, 2, 3, 4]))
    assert_equal([3, 3], @n::SingletonMethods.hash_instance_methods_return_size({a: 2, b: 4, c: 8}))

    assert_raise(NoMethodError) { String.greeting }
    assert_raise(NoMethodError) { Integer.answer }
    assert_raise(NoMethodError) { Array.blank }
    assert_raise(NoMethodError) { Hash.http_200 }
  end

  def test_add_constants_in_box
    pend unless Ruby::Box.enabled?

    String.const_set(:STR_CONST0, 999)
    assert_equal 999, String::STR_CONST0
    assert_equal 999, String.const_get(:STR_CONST0)

    assert_raise(NameError) { String.const_get(:STR_CONST1) }
    assert_raise(NameError) { String::STR_CONST2 }
    assert_raise(NameError) { String::STR_CONST3 }
    assert_raise(NameError) { Integer.const_get(:INT_CONST1) }

    EnvUtil.suppress_warning do
      @n.require_relative('namespace/consts')
    end
    assert_equal 999, String::STR_CONST0
    assert_raise(NameError) { String::STR_CONST1 }
    assert_raise(NameError) { String::STR_CONST2 }
    assert_raise(NameError) { Integer::INT_CONST1 }

    assert_not_nil @n::ForConsts.refer_all

    assert_equal 112, @n::ForConsts.refer1
    assert_equal 112, @n::ForConsts.get1
    assert_equal 112, @n::ForConsts::CONST1
    assert_equal 222, @n::ForConsts.refer2
    assert_equal 222, @n::ForConsts.get2
    assert_equal 222, @n::ForConsts::CONST2
    assert_equal 333, @n::ForConsts.refer3
    assert_equal 333, @n::ForConsts.get3
    assert_equal 333, @n::ForConsts::CONST3

    EnvUtil.suppress_warning do
      @n::ForConsts.const_set(:CONST3, 334)
    end
    assert_equal 334, @n::ForConsts::CONST3
    assert_equal 334, @n::ForConsts.refer3
    assert_equal 334, @n::ForConsts.get3

    assert_equal 10, @n::ForConsts.refer_top_const

    # use Proxy object to use usual methods instead of singleton methods
    proxy = @n::ForConsts::Proxy.new

    assert_raise(NameError){ proxy.call_str_refer0 }
    assert_raise(NameError){ proxy.call_str_get0 }

    proxy.call_str_set0(30)
    assert_equal 30, proxy.call_str_refer0
    assert_equal 30, proxy.call_str_get0
    assert_equal 999, String::STR_CONST0

    proxy.call_str_remove0
    assert_raise(NameError){ proxy.call_str_refer0 }
    assert_raise(NameError){ proxy.call_str_get0 }

    assert_equal 112, proxy.call_str_refer1
    assert_equal 112, proxy.call_str_get1
    assert_equal 223, proxy.call_str_refer2
    assert_equal 223, proxy.call_str_get2
    assert_equal 333, proxy.call_str_refer3
    assert_equal 333, proxy.call_str_get3

    EnvUtil.suppress_warning do
      proxy.call_str_set3
    end
    assert_equal 334, proxy.call_str_refer3
    assert_equal 334, proxy.call_str_get3

    assert_equal 1, proxy.refer_int_const1

    assert_equal 999, String::STR_CONST0
    assert_raise(NameError) { String::STR_CONST1 }
    assert_raise(NameError) { String::STR_CONST2 }
    assert_raise(NameError) { String::STR_CONST3 }
    assert_raise(NameError) { Integer::INT_CONST1 }
  end

  def test_global_variables
    default_l = $-0
    default_f = $,

    pend unless Ruby::Box.enabled?

    assert_equal "\n", $-0 # equal to $/, line splitter
    assert_equal nil, $,   # field splitter

    @n.require_relative('namespace/global_vars')

    # read first
    assert_equal "\n", @n::LineSplitter.read
    @n::LineSplitter.write("\r\n")
    assert_equal "\r\n", @n::LineSplitter.read
    assert_equal "\n", $-0

    # write first
    @n::FieldSplitter.write(",")
    assert_equal ",", @n::FieldSplitter.read
    assert_equal nil, $,

    # used only in ns
    assert !global_variables.include?(:$used_only_in_ns)
    @n::UniqueGvar.write(123)
    assert_equal 123, @n::UniqueGvar.read
    assert_nil $used_only_in_ns

    # Kernel#global_variables returns the sum of all gvars.
    global_gvars = global_variables.sort
    assert_equal global_gvars, @n::UniqueGvar.gvars_in_ns.sort
    @n::UniqueGvar.write_only(456)
    assert_equal (global_gvars + [:$write_only_var_in_ns]).sort, @n::UniqueGvar.gvars_in_ns.sort
    assert_equal (global_gvars + [:$write_only_var_in_ns]).sort, global_variables.sort
  ensure
    EnvUtil.suppress_warning do
      $-0 = default_l
      $, = default_f
    end
  end

  def test_load_path_and_loaded_features
    pend unless Ruby::Box.enabled?

    assert $LOAD_PATH.respond_to?(:resolve_feature_path)

    @n.require_relative('namespace/load_path')

    assert_not_equal $LOAD_PATH, @n::LoadPathCheck::FIRST_LOAD_PATH

    assert @n::LoadPathCheck::FIRST_LOAD_PATH_RESPOND_TO_RESOLVE

    namespace_dir = File.join(__dir__, 'namespace')
    # TODO: $LOADED_FEATURES in method calls should refer the current namespace in addition to the loading namespace.
    # assert @n::LoadPathCheck.current_loaded_features.include?(File.join(namespace_dir, 'blank1.rb'))
    # assert !@n::LoadPathCheck.current_loaded_features.include?(File.join(namespace_dir, 'blank2.rb'))
    # assert @n::LoadPathCheck.require_blank2
    # assert @n::LoadPathCheck.current_loaded_features.include?(File.join(namespace_dir, 'blank2.rb'))

    assert !$LOADED_FEATURES.include?(File.join(namespace_dir, 'blank1.rb'))
    assert !$LOADED_FEATURES.include?(File.join(namespace_dir, 'blank2.rb'))
  end

  def test_eval_basic
    pend unless Ruby::Box.enabled?

    # Test basic evaluation
    result = @n.eval("1 + 1")
    assert_equal 2, result

    # Test string evaluation
    result = @n.eval("'hello ' + 'world'")
    assert_equal "hello world", result
  end

  def test_eval_with_constants
    pend unless Ruby::Box.enabled?

    # Define a constant in the namespace via eval
    @n.eval("TEST_CONST = 42")
    assert_equal 42, @n::TEST_CONST

    # Constant should not be visible in main namespace
    assert_raise(NameError) { TEST_CONST }
  end

  def test_eval_with_classes
    pend unless Ruby::Box.enabled?

    # Define a class in the namespace via eval
    @n.eval("class TestClass; def hello; 'from namespace'; end; end")

    # Class should be accessible in the namespace
    instance = @n::TestClass.new
    assert_equal "from namespace", instance.hello

    # Class should not be visible in main namespace
    assert_raise(NameError) { TestClass }
  end

  def test_eval_isolation
    pend unless Ruby::Box.enabled?

    # Create another namespace
    n2 = Namespace.new

    # Define different constants in each namespace
    @n.eval("ISOLATION_TEST = 'first'")
    n2.eval("ISOLATION_TEST = 'second'")

    # Each namespace should have its own constant
    assert_equal "first", @n::ISOLATION_TEST
    assert_equal "second", n2::ISOLATION_TEST

    # Constants should not interfere with each other
    assert_not_equal @n::ISOLATION_TEST, n2::ISOLATION_TEST
  end

  def test_eval_with_variables
    pend unless Ruby::Box.enabled?

    # Test local variable access (should work within the eval context)
    result = @n.eval("x = 10; y = 20; x + y")
    assert_equal 30, result
  end

  def test_eval_error_handling
    pend unless Ruby::Box.enabled?

    # Test syntax error
    assert_raise(SyntaxError) { @n.eval("1 +") }

    # Test name error
    assert_raise(NameError) { @n.eval("undefined_variable") }

    # Test that namespace is properly restored after error
    begin
      @n.eval("raise RuntimeError, 'test error'")
    rescue RuntimeError
      # Should be able to continue using the namespace
      result = @n.eval("2 + 2")
      assert_equal 4, result
    end
  end

  # Tests which run always (w/o RUBY_BOX=1 globally)

  def test_prelude_gems_and_loaded_features
    assert_in_out_err([ENV_ENABLE_BOX, "--enable=gems"], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
      begin;
        puts ["before:", $LOADED_FEATURES.select{ it.end_with?("/bundled_gems.rb") }&.first].join
        puts ["before:", $LOADED_FEATURES.select{ it.end_with?("/error_highlight.rb") }&.first].join

        require "error_highlight"

        puts ["after:", $LOADED_FEATURES.select{ it.end_with?("/bundled_gems.rb") }&.first].join
        puts ["after:", $LOADED_FEATURES.select{ it.end_with?("/error_highlight.rb") }&.first].join
      end;

      # No additional warnings except for experimental warnings
      assert_includes error.join("\n"), EXPERIMENTAL_WARNINGS
      assert_equal 2, error.size

      assert_includes output.grep(/^before:/).join("\n"), '/bundled_gems.rb'
      assert_includes output.grep(/^before:/).join("\n"), '/error_highlight.rb'
      assert_includes output.grep(/^after:/).join("\n"), '/bundled_gems.rb'
      assert_includes output.grep(/^after:/).join("\n"), '/error_highlight.rb'
    end
  end

  def test_prelude_gems_and_loaded_features_with_disable_gems
    assert_in_out_err([ENV_ENABLE_BOX, "--disable=gems"], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
      begin;
        puts ["before:", $LOADED_FEATURES.select{ it.end_with?("/bundled_gems.rb") }&.first].join
        puts ["before:", $LOADED_FEATURES.select{ it.end_with?("/error_highlight.rb") }&.first].join

        require "error_highlight"

        puts ["after:", $LOADED_FEATURES.select{ it.end_with?("/bundled_gems.rb") }&.first].join
        puts ["after:", $LOADED_FEATURES.select{ it.end_with?("/error_highlight.rb") }&.first].join
      end;

      assert_includes error.join("\n"), EXPERIMENTAL_WARNINGS
      assert_equal 2, error.size

      refute_includes output.grep(/^before:/).join("\n"), '/bundled_gems.rb'
      refute_includes output.grep(/^before:/).join("\n"), '/error_highlight.rb'
      refute_includes output.grep(/^after:/).join("\n"), '/bundled_gems.rb'
      assert_includes output.grep(/^after:/).join("\n"), '/error_highlight.rb'
    end
  end

  def test_root_and_main_methods
    assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      pend unless Ruby::Box.respond_to?(:root) and Ruby::Box.respond_to?(:main) # for RUBY_DEBUG > 0

      assert Ruby::Box.root.respond_to?(:root?)
      assert Ruby::Box.main.respond_to?(:main?)

      assert Ruby::Box.root.root?
      assert Ruby::Box.main.main?
      assert_equal Ruby::Box.main, Ruby::Box.current

      $a = 1
      $LOADED_FEATURES.push("/tmp/foobar")

      assert_equal 2, Ruby::Box.root.eval('$a = 2; $a')
      assert !Ruby::Box.root.eval('$LOADED_FEATURES.push("/tmp/barbaz"); $LOADED_FEATURES.include?("/tmp/foobar")')
      assert "FooClass", Ruby::Box.root.eval('class FooClass; end; Object.const_get(:FooClass).to_s')

      assert_equal 1, $a
      assert !$LOADED_FEATURES.include?("/tmp/barbaz")
      assert !Object.const_defined?(:FooClass)
    end;
  ensure
    tmp = ENV["TMPDIR"] || ENV["TMP"] || Etc.systmpdir || "/tmp"
    pat = "_ruby_ns_*."+RbConfig::CONFIG["DLEXT"]
    File.unlink(*Dir.glob(pat, base: tmp).map {|so| "#{tmp}/#{so}"})
  end

  def test_basic_box_detections
    assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      ns = Ruby::Box.new
      $gvar1 = 'bar'
      code = <<~EOC
      NS1 = Ruby::Box.current
      $gvar1 = 'foo'

      def toplevel = $gvar1

      class Foo
        NS2 = Ruby::Box.current
        NS2_proc = ->(){ NS2 }
        NS3_proc = ->(){ Ruby::Box.current }

        def ns4 = Ruby::Box.current
        def self.ns5 = NS2
        def self.ns6 = Ruby::Box.current
        def self.ns6_proc = ->(){ Ruby::Box.current }
        def self.ns7
          res = []
          [1,2].chunk{ it.even? }.each do |bool, members|
            res << Ruby::Box.current.object_id.to_s + ":" + bool.to_s + ":" + members.map(&:to_s).join(",")
          end
          res
        end

        def self.yield_block = yield
        def self.call_block(&b) = b.call

        def self.gvar1 = $gvar1
        def self.call_toplevel = toplevel
      end
      FOO_NAME = Foo.name

      module Kernel
        def foo_box = Ruby::Box.current
        module_function :foo_box
      end

      NS_X = Foo.new.ns4
      NS_Y = foo_box
      EOC
      ns.eval(code)
      outer = Ruby::Box.current
      assert_equal ns, ns::NS1 # on TOP frame
      assert_equal ns, ns::Foo::NS2 # on CLASS frame
      assert_equal ns, ns::Foo::NS2_proc.call # proc -> a const on CLASS
      assert_equal ns, ns::Foo::NS3_proc.call # proc -> the current
      assert_equal ns, ns::Foo.new.ns4 # instance method  -> the current
      assert_equal ns, ns::Foo.ns5     # singleton method -> a const on CLASS
      assert_equal ns, ns::Foo.ns6     # singleton method -> the current
      assert_equal ns, ns::Foo.ns6_proc.call # method returns a proc -> the current

      # a block after CFUNC/IFUNC in a method -> the current
      assert_equal ["#{ns.object_id}:false:1", "#{ns.object_id}:true:2"], ns::Foo.ns7

      assert_equal outer, ns::Foo.yield_block{ Ruby::Box.current } # method yields
      assert_equal outer, ns::Foo.call_block{ Ruby::Box.current }  # method calls a block

      assert_equal 'foo', ns::Foo.gvar1 # method refers gvar
      assert_equal 'bar', $gvar1        # gvar value out of the ns
      assert_equal 'foo', ns::Foo.call_toplevel # toplevel method referring gvar

      assert_equal ns, ns::NS_X # on TOP frame, referring a class in the current
      assert_equal ns, ns::NS_Y # on TOP frame, referring Kernel method defined by a CFUNC method

      assert_equal "Foo", ns::FOO_NAME
      assert_equal "Foo", ns::Foo.name
    end;
  end

  def test_loading_extension_libs_in_main_box
    pend if /mswin|mingw/ =~ RUBY_PLATFORM # timeout on windows environments
    assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      require "prism"
      require "optparse"
      require "date"
      require "time"
      require "delegate"
      require "singleton"
      require "pp"
      require "fileutils"
      require "tempfile"
      require "tmpdir"
      require "json"
      require "psych"
      require "yaml"
      require "zlib"
      require "open3"
      require "ipaddr"
      require "net/http"
      require "openssl"
      require "socket"
      require "uri"
      require "digest"
      require "erb"
      require "stringio"
      require "monitor"
      require "timeout"
      require "securerandom"
      expected = 1
      assert_equal expected, 1
    end;
  end

  def test_mark_box_object_referred_only_from_binding
    assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
    begin;
      box = Ruby::Box.new
      box.eval('class Integer; def +(*)=42; end')
      b = box.eval('binding')
      box = nil # remove direct reference to the box

      assert_equal 42, b.eval('1+2')

      GC.stress = true
      GC.start

      assert_equal 42, b.eval('1+2')
    end;
  end
end
